crackmeUK 解析手引き その2(easy)
今回は EASY についての解説です。 前回デバッガチェック部分の解説を行いましたが、デバッガチェックを回避するパッチは すでに当てていますか?そうしないと解析ができません。 未だの方は、解析手引き その1 を御覧になり、パッチを当ててから進んで下さい。 crackme を起動してみると、マシンによって固有 ID が生成されるようですが、 まずこの ID はどのように生成されているかを調べてみる必要がありそうです。 マシン固有 ID の生成には様々な方法がありますが、今回は『eagle0wl's crackme VOL.01』の crackme #10 で 固有 ID 生成のために用いられていた GetVolumeInformationA にブレークポイントを仕掛けます。 右クリック→「検索」→「ラベル一覧」(「Search for」→「Name(Label)」)で、ラベル一覧の一覧を表示します。 Names in CrackMe Address Section Type ( Name Comment 00404E52 .text Export <ModuleEntryPoint> 0041C000 .rdata Import ( ADVAPI32.RegCreateKeyExA 0041C004 .rdata Import ( ADVAPI32.RegCloseKey 0041C008 .rdata Import ( ADVAPI32.RegOpenKeyExA 0041C00C .rdata Import ( ADVAPI32.RegSetValueExA 0041C014 .rdata Import ( COMCTL32.#17 (〜中略) 0041C170 .rdata Import ( KERNEL32.GetWindowsDirectoryA 0041C174 .rdata Import ( KERNEL32.GetVolumeInformationA 0041C178 .rdata Import ( KERNEL32.GetModuleHandleA 0041C17C .rdata Import ( KERNEL32.GetModuleFileNameA 0041C180 .rdata Import ( KERNEL32.CreateFileA (〜中略) 0041C46C .rdata Import ( USER32.HideCaret 0041C474 .rdata Import WINSPOOL.DocumentPropertiesA 0041C478 .rdata Import WINSPOOL.OpenPrinterA 0041C47C .rdata Import WINSPOOL.ClosePrinter 0041C484 .rdata Import ( comdlg32.GetFileTitleA 「0041C174 .rdata Import ( KERNEL32.GetVolumeInformationA」を選択して、「Enter」キーを押してください。 「References in CrackMe:.text to KERNEL32.GetVolumeInformationA」というウィンドウが表示されます。 References in CrackMe:.text to KERNEL32.GetVolumeInformationA Address Disassembly Comment 0040181D CALL DWORD PTR DS:[<&KERNEL32.GetVolumeI kernel32.GetVolumeInformationA 0041445E CALL DWORD PTR DS:[<&KERNEL32.GetVolumeI kernel32.GetVolumeInformationA とりあえず両方にBPを仕掛けてください。 「F9」キーで実行させます。 すると 0041445E でブレークしました。画面右下のスタックウィンドウを見て下さい。 * スタックウィンドウのアドレスは環境(OS)によって変わりますので注意して下さい。 0065F58C 00884250 |RootPathName = "C:\" 0065F590 00000000 |VolumeNameBuffer = NULL 0065F594 00000000 |MaxVolumeNameSize = 0 0065F598 00000000 |pVolumeSerialNumber = NULL 0065F59C 0065F6F4 |pMaxFilenameLength = 0065F6F4 0065F5A0 0065F6FC |pFileSystemFlags = 0065F6FC 0065F5A4 00000000 |pFileSystemNameBuffer = NULL 0065F5A8 00000000 \pFileSystemNameSize = NULL これを見る限り、NULL になっているので、マシン固有情報を取っていないようです。 F9 キーで飛ばしましょう。 すると再び同じ所で停止しますがもう一度 F9 を押して下さい。 すると『This is "Crack Me". Created By UK』という表示が出ました。 OK ボタンを押すと 0040181D でブレークしました。スタックウィンドウを見てみましょう。 0065F5C8 0065F604 |RootPathName = "C:\" 0065F5CC 00000000 |VolumeNameBuffer = NULL 0065F5D0 00000000 |MaxVolumeNameSize = 0 0065F5D4 0065FD6C |pVolumeSerialNumber = 0065FD6C ; あやしい 0065F5D8 00000000 |pMaxFilenameLength = NULL 0065F5DC 00000000 |pFileSystemFlags = NULL 0065F5E0 00000000 |pFileSystemNameBuffer = NULL 0065F5E4 00000000 \pFileSystemNameSize = NULL NULL が続く中、pVolumeSerialNumber 【だけ】に(ローカル変数の)アドレスが入っています。 (このアドレスは環境によって変化するので各自頭の中で置き換えてください) 非常に怪しいですね。画面右下のダンプウィンドウにフォーカスを合わせ、Ctrl + G から 0065FD6C (この値は各自置き換えること)と入力して、該当部分を参照して下さい。 次に F8 を押して GetVolumeInformationA を押して実行すると、このアドレスに値が入ります。 0065FD6C 98 56 12 AB ←この値は一例です。 ^^^^^^^^^^^ 取得された値は AB125698h であることがわかります。 さて、表示された ID はというと、 AB-125698 というようにほぼそのままの状態になっているはずです。 ちなみに値を取得した直後にこの値を書き換えると ID も変わりました。 ということは、VolumeSerialNumber から ID を生成しているとほぼ確定されたということになります。 とりあえず、固有 ID 生成部分については終わりです。 続いてキーチェックの方へ行ってみたいと思います。 今回は EASY について解説ですので、ラジオボタンの EASY にチェックが入っているか確認してください。 次に、パスワード入力欄にフェイクパス "9876543210" を入力した後に 定番 API 、GetWindowsTextA にブレークポイントを仕掛けてから OK ボタンを押してみましょう。 00415460 |. 50 push eax ; |Buffer 00415461 |. FF76 1C push dword ptr ds:[esi+1C] ; |hWnd 00415464 |. FF15 30C44100 call near dword ptr ds:[<&USER32.GetWindowTextA>; \GetWindowTextA 実はこの命令の後、別の処理で 19 回 GetWindowTextA が呼び出されているのですが、 この API コールで本当に入力パスが取得されるのか確認してみることにしましょう。 画面右下のスタックウィンドウを見て下さい。 0065F790 000003F8 |hWnd = 000003F8 (class='Edit',wndproc=807ECAA8,parent=00000FF8) 0065F794 0088C844 |Buffer = 0088C844 <-- 入力パスが格納されるバッファのアドレス 0065F798 00000009 \Count = 9 まだ GetWindowTextA は実行されていません。入力パスが格納されるバッファのアドレスが 上から2番目の列に見えます。 この解説は、WindowsMe 上で解析を行って書かれたものを編集しているのですが、バッファのアドレスは 環境によって変わりますので各自置き換えて読んで下さい。 それでは、画面左下のダンプウィンドウにフォーカスを合わせてから、Ctrl + G を押して バッファのアドレスを打ち込んでジャンプしましょう。 ジャンプできましたか? それでは F8 キーを押して GetWindowTextA を実行して下さい。 するとバッファに入力パスが表示されました。チェックルーチンはこの先にありそうです。 ちなみにダンプウィンドウの表示アドレスはそのままにして F9 を連打すると(今はしないでね)、 そのたびにブレークするわけですが(計 19 回)、登録失敗時に表示される『Incorrect Password』の 文字列が踊る様子を見ることが出来て面白いです。 本題に戻ります。GetWindowTextA を抜けたところで、call 命令がひとつありますが、 構わず F8 キーで飛ばして ret 命令 (00415484) まで進めていきましょう。 ret 命令でジャンプするとアドレス 00401A7E に出るはずです。 00401A7E |. 8B4C24 10 mov ecx, dword ptr ss:[esp+10] さて、ここからが実質的なキーチェックルーチンです。気を引き締めてかかりましょう。 いきなりアドレス 00401A88 で GetTickCount 命令が呼び出されています。 起動時のデバッガチェックで GetTickCount が使用されていたことを思い出して下さい。 どうやらキーチェック部分でも同様のチェッカが存在しているようです。 そんなことを思い出しながら F8 キーを軽快に叩いていきましょう。 まもなく以下のような比較分岐命令が現れてきました。 00401A96 |. 8B41 F8 mov eax, dword ptr ds:[ecx-8] ; eax = 入力パスの文字数 00401A99 |. 83F8 10 cmp eax, 10 ; 入力パスの文字数は 16 文字か? 00401A9C |. 75 60 jnz short CRACKME.00401AFE ; 16 文字以外なら登録失敗 00401A9E |. 8039 43 cmp byte ptr ds:[ecx], 43 ; 入力パス一文字目は 43h = 'C' か? 00401AA1 |. 75 0B jnz short CRACKME.00401AAE 00401AA3 |. 8079 01 52 cmp byte ptr ds:[ecx+1], 52 ; 入力パス二文字目は 52h = 'R' か? 00401AA7 |. 75 05 jnz short CRACKME.00401AAE 00401AA9 |. C64424 14 01 mov byte ptr ss:[esp+14], 1 ; 1を [esp+14] に入れているが、、、 まず入力パスの文字数チェックを行っています。文字数を取得できる API が近くに見あたらないのでわかりにくいですが、 パスチェックを行う上で最初にやることは文字数チェックと相場が決まっていますので、そう予測しました。 不安なら後で文字数を変えて再度入力して確認してみてください。 次に入力パスの先頭2文字が "CR" であるかどうかをチェックしているわけですが、ひとまずゼロフラグを反転して続けましょう。 無事通過すると、なぜか1を [esp+14] (ローカル変数)に入れていて非常に怪しいです。 ([esp+14] はアドレス 00401A67 にて初期化されている)。ここは要チェックですね。 次にアドレス 00401AB6 に call 命令がありますがとりあえず F8 で飛ばしてみましょう。 00401AAE |> 6A 02 push 2 ; /Arg2 = 00000002 ; 削りたい文字数 00401AB0 | 6A 00 push 0 ; |Arg1 = 00000000 ; 何文字目から削るか 00401AB2 |. 8D4C24 18 lea ecx, dword ptr ss:[esp+18] ; | 00401AB6 |. E8 47010100 call CRACKME.00411C02 ; \CRACKME.00411C02 call 命令通過後、 ecx レジスタを見て下さい。[ecx] = "76543210" というように、先頭2文字が削れています。 前回、「殆どのコンパイラは関数の戻り値を eax レジスタに格納している」と説明しましたが、今回戻り値を格納するために ecx レジスタを使用したという訳ではありません。 ecx レジスタに偶然そういう値が入っただけに過ぎません。詳しくは自分で call 内部に潜って確認してみて下さい。 続けてトレースしていきましょう。 00401ABB |. 8B8E D0000000 mov ecx, dword ptr ds:[esi+D0] ; 固有 ID 値を ecx レジスタに 00401AC1 |. 33FF xor edi, edi ; ループに入る前に edi を初期化している 00401AC3 |. BE 01000000 mov esi, 1 ; esi も初期化している模様 00401AC8 |> 8BC1 /mov eax, ecx <-----------------+ 00401ACA |. 33D2 |xor edx, edx | 00401ACC |. BB 0A000000 |mov ebx, 0A | 00401AD1 |. F7F3 |div ebx | 00401AD3 |. B8 CDCCCCCC |mov eax, CCCCCCCD | 00401AD8 |. 03FA |add edi, edx | 00401ADA |. 42 |inc edx | 00401ADB |. 0FAFF2 |imul esi, edx | 00401ADE |. F7E1 |mul ecx | 00401AE0 |. C1EA 03 |shr edx, 3 | 00401AE3 |. 8BCA |mov ecx, edx | ループして何やら値を 00401AE5 |.^75 E1 \jnz short CRACKME.00401AC8 ----+ 生成している模様 固有 ID 値(表示が 12-AB2345 なら値は 12AB2345 となる)から何やら値を生成しているようです。 このループを見てどのように値を生成しているわかりますか? よく見てみるとループ内ではレジスタ間でのやり取りだけでスタックポインタ(ローカル変数・引数)は参照されていません。 したがって生成された値はレジスタに入ってるはずです。 ではどのレジスタに値が入っているのか。実はループ内部の処理を追いかける必要はありません。 ループ前に xor edi, edi mov esi, 1 というように edi, esi レジスタが初期化されていることに注目して下さい。 変数に値を格納するときは前もってその変数の値を初期化する必要があります。 ということは、edi, esi レジスタに生成された値が格納されているとほぼ断定できます。 前回、ebx, esi, edi レジスタの値は call 命令内部で push'n pop されているため値は保持されます。 よって そのレジスタに入った値は結構重要だということをお話ししました。 そのことを頭に入れておきつつ、続けていきましょう。 00401AE7 |. 6A 00 push 0 00401AE9 |. 68 DC214200 push CRACKME.004221DC ; [004221DC] = "-" (ハイフン) 00401AEE |. 8D4C24 18 lea ecx, dword ptr ss:[esp+18] 00401AF2 |. E8 0C030100 call CRACKME.00411E03 ; あやしげな関数 00401AF7 |. 8BE8 mov ebp, eax ; 戻り値を ebp レジスタに格納 00401AF9 |. 83FD FF cmp ebp, -1 ; (ebp = call 命令の戻り値) が -1 以外なら OK 00401AFC |. 75 1B jnz short CRACKME.00401B19 --> ジャンプすることで継続 この call ではパスワード文字列 "76543210" の中からハイフンの位置を探しています。 なぜハイフンの位置を探す関数であることがわかるのかと言いますと、call 内部をトレースして 戻り値が -1 以外ならOK ということですが、戻り値に -1 が返ることのある文字列処理系の関数というと 文字列比較(s1 < s2 の時)、文字列検索(該当文字列が見つからない時)当たりでしょうか。 さらにプッシュされている値を見ると、 00401AE9 |. 68 DC214200 push CRACKME.004221DC ; [004221DC] = "-" (ハイフン) この行に、Dump ウィンドウにフォーカスを合わせ、Ctrl + G | 004221DC と打ってみると、該当部分には ハイフン一文字がありました。となると、ハイフンを探す命令ではないかと推測できるというわけです。 ここで確認、入力したフェイクパスは "9876543210" です。ハイフンはありません。 仕掛けてあるブレークポイントを全て解除してから、call 命令(00401AF2)に BP を仕掛けた後、 一旦登録に失敗してから再度フェイクパス "CR9876-543210987" と打ち込んでもう一度登録ボタンを押してみましょう。 すると今仕掛けた BP でブレークしました。続けてトレースしていきましょう。 今度はハイフンがあるので 00401B19 にジャンプして処理が続きます。 00401B23 に call 命令がありますが、引数は入力パスとは関係なさそうな上、call 命令の直後に比較分岐命令がないので、 素通りしていきましょう。 00401B32 で再び call 命令です。 00401B2E |. 6A 10 push 10 00401B30 |. 51 push ecx 00401B31 |. 50 push eax ; [eax] = "9876" 00401B32 |. E8 302F0000 call CRACKME.00404A67 ; あやしい call 00401B37 |. 83C4 0C add esp, 0C 00401B3A |. 3BF8 cmp edi, eax ; edi = 固有IDから生成された値, eax = 関数戻り値 00401B3C |. 8D4C24 1C lea ecx, dword ptr ss:[esp+1C] 00401B40 |. 0F94C3 sete bl 3文字目からハイフン直前までの文字列のポインタがプッシュされています。 さらに call を抜けた後に比較命令があり、 edi レジスタ、これは固有IDから生成された値です。 非常に怪しいです。F8 で call を抜けてみると、その戻り値は eax = 00009876 となっています。 ここは、文字列([0-9][A-F][a-f])から値に変換するものでした。 ということは、edi の値に合わせて入力パスを変えれば OK です。 (edi = 2D なら、"CR002D-543210987" といった具合に変える) 00401B3A |. 3BF8 cmp edi, eax ; 比較した結果 00401B3C |. 8D4C24 1C lea ecx, dword ptr ss:[esp+1C] 00401B40 |. 0F94C3 sete bl ; 等しければ bl = 1, 等しくないのなら bl = 0 が入る 00401B43 |. E8 CA590100 call CRACKME.00417512 ; call 命令があるが、ebx レジスタの値は保持される 00401B48 |. 84DB test bl, bl ; bl = 1 なら 00401B4A |. 74 05 je short CRACKME.00401B51 ; ここでジャンプしない 00401B4C |. C64424 16 01 mov byte ptr ss:[esp+16], 1 ; またローカル変数に 1 を代入している 00401B51 |> 45 inc ebp 比較した結果が等しければ、00401B4C でまたローカル変数に 1 を代入しています。この 1 は TRUE で、 最後にまとめてチェックするのでは無いかと予想できます。 00401B52 |. 8D4C24 10 lea ecx, dword ptr ss:[esp+10] 00401B56 |. 55 push ebp ; /Arg2 00401B57 |. 6A 00 push 0 ; |Arg1 = 00000000 00401B59 |. E8 A4000100 call CRACKME.00411C02 ; \CRACKME.00411C02 call を抜けるとレジスタの値は eax = 0000000E, [ecx] = "543210987" となっていました。 これはハイフン以降の文字列です。 ホントは call 内部もトレースするべきなのですが、 解析時間の短縮のためにもこういった予想外の情報も活用するべきです。 00401B5E |. 6A 00 push 0 00401B60 |. 68 DC214200 push CRACKME.004221DC ; [004221DC] = "-"(ハイフン) 00401B65 |. 8D4C24 18 lea ecx, dword ptr ss:[esp+18] 00401B69 |. E8 95020100 call CRACKME.00411E03 ; ハイフンチェック再び 00401B6E |. 83CF FF or edi, FFFFFFFF ; これは mov edi, FFFFFFFF と同じ 00401B71 |. 3BC7 cmp eax, edi ; ハイフンが見つからない(eax = FFFFFFFF)と 00401B73 |. 0F84 8B000000 je CRACKME.00401C04 ; ジャンプして登録失敗 またハイフンチェックです。 現在仕掛けられているブレークポイントを解除した後、 call 部分にブレークポイントを仕掛け、 "CR002D-543210987" を "CR002D-5432-1098" とでも変えて再び試してみましょう。 (文字数は16文字であること、002D は固有 ID から生成された値からであることに注意。002D の部分は各自置き換えて下さい) もう少しです。頑張りましょう。 00401B79 |. 40 inc eax 00401B7A |. 8D4C24 10 lea ecx, dword ptr ss:[esp+10] 00401B7E |. 50 push eax ; /Arg2 00401B7F |. 6A 00 push 0 ; |Arg1 = 00000000 00401B81 |. E8 7C000100 call CRACKME.00411C02 ; \CRACKME.00411C02 00401B86 |. 8B4424 10 mov eax, dword ptr ss:[esp+10] ; [esp+10] は ecx と同じ値 00401B8A |. 8D5424 20 lea edx, dword ptr ss:[esp+20] call を抜けた後のレジスタの値は eax = 00000009, [ecx] = "1098" となっています。1098 は "CR002D-5432-1098" ^^^^ この部分ですね。 直後の mov eax, dword ptr ss:[esp+10] で同じ値が代入されています。 00401B8E |. 6A 10 push 10 00401B90 |. 52 push edx 00401B91 |. 50 push eax ; [eax] = "1098" 00401B92 |. E8 D02E0000 call CRACKME.00404A67 ; 文字列([0-9][A-F][a-f])から値に変換する 00401B97 |. 83C4 0C add esp, 0C 00401B9A |. 3BF0 cmp esi, eax ; esi = 固有IDから生成された値, eax = 関数戻り値 00401B9C |. B3 01 mov bl, 1 ; bl に 1 が代入される 00401B9E |. 74 04 je short CRACKME.00401BA4 ; esi = eax ならばジャンプ -----------------+ 00401BA0 |. 8A5C24 17 mov bl, byte ptr ss:[esp+17] ; [esp+17] は 0 (00401A5A で初期化されている)| 00401BA4 |> FF15 24C24100 call near dword ptr ds:[<&KERNEL32.GetTickCount>; [GetTickCount] <---------+ 00401BAA |. 8A4C24 14 mov cl, byte ptr ss:[esp+14] ; 入力パス先頭2文字は 00401BAE |. 84C9 test cl, cl ; 'CR' だったか 00401BB0 |. 74 52 je short CRACKME.00401C04 00401BB2 |. 8A4C24 16 mov cl, byte ptr ss:[esp+16] ; "CRxxxx-yyyy-zzzzz" 00401BB6 |. 84C9 test cl, cl ; xxxx の比較の結果 00401BB8 |. 74 4A je short CRACKME.00401C04 00401BBA |. 84DB test bl, bl ; esi, eax の比較の結果、 00401BBC |. 74 46 je short CRACKME.00401C04 ; 等しければジャンプしない 00401BBE |. 2B4424 24 sub eax, dword ptr ss:[esp+24] ; キーチェック部分の実行時間算出 00401BC2 |. 83F8 64 cmp eax, 64 ; 100 ミリ秒以内なら 00401BC5 |. 73 3D jnb short CRACKME.00401C04 ; ジャンプしない 00401BC7 |. 68 00224200 push CRACKME.00422200 ; ASCII "Congratulations!" 最後は比較分岐が非常に多く、入り組んだ形となっています。 キーチェック部分で mov byte ptr ss:[esp+**], 1 というような命令が何度かありましたが、 これは入力パスとを比較後、すぐに不正解処理に飛ばさずに一旦ローカル変数に比較結果を 正解 = 1 (TRUE), 不正解 = 0 (FALSE)として保存し、最後にまとめてチェックしているためです。 コードの右にコメントを入れたのでじっくりと読んで下さい。 一番最後に、 00401BA4 |> FF15 24C24100 call near dword ptr ds:[<&KERNEL32.GetTickCount>; [GetTickCount] (中略) 00401BBE |. 2B4424 24 sub eax, dword ptr ss:[esp+24] ; キーチェック部分の実行時間算出 00401BC2 |. 83F8 64 cmp eax, 64 ; 100 ミリ秒以内なら デバッガチェック部分にあったトレースチェッカと同じものですね。前回説明したので自分で理解して下さい。 これより、以下のことがわかりました。 ・固有 ID は GetVolumeInformationA の pVolumeSerialNumber から取得。 pVolumeSerialNumber = 54AB32CD なら 54-AB32CD と表示されます。 ・正解パスは16文字 ・正解パスの形式は CRxxxx-yyyy-zzzz で、ハイフンが2ヶ所あり、xxxx,yyyy,zzzz には [0-9][A-F][a-f] の範囲で文字が入り、xxxx,yyyy,zzzz 各部分の文字列長は決まっていません。 (全体が16文字であればよい)。 ・アドレス 00401ABB - 00401AE5 の間のループで命令固有 ID から2つの値を算出し、 その値はedi, esi レジスタにそれぞれ入ります。 ・xxxx と edi の値を比較。edi = 00000012 であれば xxxx は "12" または"00012" といった風になります。 同様に zzzz と esi の値を比較。 esi = 0001AB49 であればzzzz は "1AB49" または "01AB49" といった風になります。 yyyy の比較命令は無いのでここは任意となります。ただしこの部分にハイフンは使えません(フォーマット違反)。 ■まとめ アドレス 00401AE7 にブレークポイントを仕掛けて OK ボタンを押すとブレークするはずなので、 その状態での edi レジスタと esi レジスタの値を読み、その値が edi = 0000012, esi = 0001AB49 だとすると、 正解パスは"CR12-xxxx-01AB49", "CR000012-x-1AB49", "CR0012--0001AB49", などが考えられます。 (x についてはハイフン以外の任意の文字) cr1111-1111-1111でもトレースで,edi,esiから正解パスが出てきます。